﻿using System;
using System.Globalization;
using REGEX = System.Text.RegularExpressions;

namespace BoresSoft.Ftp
{
    // UNIX-style listing (UNIX)
    // "-rw-r--r--   1 root     other        531 Jan 29 03:26 README"
    // "dr-xr-xr-x   2 root     other        512 Apr  8  1994 etc"
    // "dr-xr-xr-x   2 root     512 Apr  8  1994 etc"
    // "lrwxrwxrwx   1 root     other          7 Jan 25 00:17 bin -> usr/bin"
    // "-rw-r--r--   1 root     root       46508 Dec  8  2006 2006shoot.jpg"
    // UNIX ls does not show the year for dates in the last six months. So we have to guess the year.
    
    // Microsoft's FTP servers for Windows:
    // "----------   1 owner    group         1803128 Jul 10 10:18 ls-lR.Z"
    // "d---------   1 owner    group               0 May  9 19:45 Softlib"
    
    // WFTPD for MSDOS:
    // "-rwxrwxrwx   1 noone    nogroup      322 Aug 19  1996 message.ftp"
    // NetWare:    ' "d [R----F--] supervisor            512       Jan 16 18:53    login"
    // "- [R----F--] rhesus             214059       Oct 20 15:27    cx.exe"
    // NetPresenz for the Mac:    ' "-------r--         326  1391972  1392298 Nov 22  1995 MegaPhone.sit"
    // "drwxrwxr-x               folder        2 May 10  1996 network"
    
    // MultiNet (some spaces removed from examples)    ' "00README.TXT;1      2 30-DEC-1996 17:44 [SYSTEM] (RWED,RWED,RE,RE)"
    // "CORE.DIR;1          1  8-SEP-1996 16:09 [SYSTEM] (RWE,RWE,RE,RE)"
    
    // and non-MutliNet VMS:
    // "CII-MANUAL.TEX;1  213/216  29-JAN-1996 03:33:12  [ANONYMOU,ANONYMOUS]   (RWED,RWED,,)"
    
    // MSDOS format (Windows_NT)
    // 04-27-00  09:09PM       <DIR>          licensed
    // 07-18-00  10:16AM       <DIR>          pub
    // 04-14-00  03:47PM                  589 readme.htm

	// MLSD format (any supporting server)
	// ???

	/// <summary>
	/// Represents one file system item on the remote server.
	/// </summary>
	public sealed class ItemInfo
	{
		private const REGEX.RegexOptions regexOptions =
			REGEX.RegexOptions.IgnoreCase |
			REGEX.RegexOptions.Singleline |
			REGEX.RegexOptions.ExplicitCapture |
			REGEX.RegexOptions.Compiled;

        private static readonly REGEX.Regex reUnix = new REGEX.Regex(
			@"^(?<type>[a-z])\S+\s+\d+\s+\S+\s+\S+\s+(?<size>\d+)\s+(?<mon>[a-z]{3})\s+(?<day>\d{1,2})\s+(?:(?<year>\d{4})|(?<time>\d{2}:\d{2}))\s+(?<name>\S+)",
			regexOptions
		);
		private static readonly REGEX.Regex reMsDos = new REGEX.Regex(
			@"^(?<date>\d{2}-\d{2}-\d{2})\s+(?<time>\d{2}:\d{2}[ap]m)\s+(?<type>\<dir\>)?\s+(?<size>\d+)?\s+(?<name>\S+)",
			regexOptions
		);
		private static readonly REGEX.Regex reMlsd = new REGEX.Regex(
			@"^(?<name>[a-z]+)=(?<value>[^\s;]+);",
			regexOptions
		);

		internal ItemInfo(string itemInfoLine, ServerSystemType serverType)
		{
			if (null == itemInfoLine)
				throw new ArgumentNullException("itemInfoLine");
			if (0 == itemInfoLine.Length)
				throw new ArgumentException("Item info line not specified.", "itemInfoLine");

            switch (serverType)
            {
				case ServerSystemType.None: // MLSD
					ParseMlsd(itemInfoLine);
					break;
                case ServerSystemType.Unix:
					ParseUnix(itemInfoLine);
                    break;
				case ServerSystemType.MsDos:
					ParseMsDos(itemInfoLine);
					break;
                default:
                    throw new FtpException(ErrorCode.UnknownServerType);
            }
		}

		private void ParseMlsd(string itemInfoLine)
		{
			string tmpLine = itemInfoLine;
			REGEX.Match m = reMlsd.Match(itemInfoLine);

			while (m.Groups["name"].Value.Length > 0)
			{
				if ("type".Equals(m.Groups["name"].Value, StringComparison.Ordinal))
					Directory = "dir".Equals(m.Groups["value"].Value, StringComparison.OrdinalIgnoreCase);
				else if ("size".Equals(m.Groups["name"].Value, StringComparison.Ordinal))
					Size = long.Parse(m.Groups["value"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture);
				else if ("modify".Equals(m.Groups["name"].Value, StringComparison.Ordinal))
					Modified = DateTime.ParseExact(m.Groups["value"].Value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture);

				tmpLine = tmpLine.Substring(m.Groups[0].Value.Length);
				m = reMlsd.Match(tmpLine);
			}

			Name = tmpLine.Trim();
		}

        private void ParseUnix(string itemInfoLine)
        {
			REGEX.Match m = reUnix.Match(itemInfoLine);
			if (8 != m.Groups.Count)
				throw new FormatException("Invalid item info line format.");

			Name = m.Groups["name"].Value;
			Directory = "d".Equals(m.Groups["type"].Value, StringComparison.OrdinalIgnoreCase);
			Size = long.Parse(m.Groups["size"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture);
			if (0 == m.Groups["year"].Length)
			{
				Modified = DateTime.ParseExact(
					string.Format(
						CultureInfo.InvariantCulture,
						"{0} {1} {2} {3}",
						m.Groups["day"].Value,
						m.Groups["mon"].Value,
						DateTime.Today.Year,
						m.Groups["time"].Value
					),
					"d MMM yyyy HH:mm",
					CultureInfo.InvariantCulture
				);
				if (Modified > DateTime.Now)
					Modified.AddYears(-1);
			}
			else
			{
				Modified = DateTime.ParseExact(
					string.Format(
						CultureInfo.InvariantCulture,
						"{0} {1} {2}",
						m.Groups["day"].Value,
						m.Groups["mon"].Value,
						m.Groups["year"].Value
					),
					"d MMM yyyy",
					CultureInfo.InvariantCulture
				);
			}
        }

		private void ParseMsDos(string itemInfoLine)
		{
			REGEX.Match m = reMsDos.Match(itemInfoLine);
			if (6 != m.Groups.Count)
				throw new FormatException("Invalid item info line format.");

			Name = m.Groups["name"].Value;
			Directory = m.Groups["type"].Value.Length > 0;
			Size = Directory ? 0 : long.Parse(m.Groups["size"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture);
			Modified = DateTime.ParseExact(
				string.Format(
					CultureInfo.InvariantCulture,
					"{0} {1}",
					m.Groups["date"].Value,
					m.Groups["time"].Value
				),
				"MM-dd-yy hh:mmtt",
				CultureInfo.InvariantCulture
			);
		}

		/// <summary>
		/// Name of the item (including extension).
		/// </summary>
		public string Name
		{
			get;
			private set;
		}

		/// <summary>
		/// Is this a directory.
		/// </summary>
		public bool Directory
		{
			get;
			private set;
		}

		/// <summary>
		/// Item size in bytes.
		/// </summary>
		public long Size
		{
			get;
			private set;
		}

		/// <summary>
		/// Last modification time of the item.
		/// </summary>
		public DateTime Modified
		{
			get;
			private set;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			return string.Format(
				CultureInfo.CurrentCulture,
				"{0}{1}, {2}{3:g}",
				Directory ? "/" : string.Empty,
				Name,
				Directory ? string.Empty : FormatSize() + ", ",
				Modified
			);
		}

		private string FormatSize()
		{
			if (Size > 1024 * 1024 * 1024)
				return (Size / (1024 * 1024 * 1024)).ToString("#,##0.0", CultureInfo.CurrentCulture) + "Gb";
			else if (Size > 1024 * 1024)
				return (Size / (1024 * 1024)).ToString("#,##0.0", CultureInfo.CurrentCulture) + "Mb";
			else if (Size > 1024)
				return (Size / 1024).ToString("#,##0.0", CultureInfo.CurrentCulture) + "Kb";

			return Size.ToString("#,##0", CultureInfo.CurrentCulture);
		}
	}
}
